/* Created on 2006-7-21 */
package co.fitstart.entity.utils;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * String related utilities
 *
 * @author Downpour
 * @author lute
 */
public abstract class StringUtils {

    private static final Logger logger = LoggerFactory.getLogger(StringUtils.class);

    /**
     * Chars can be used to generate random string
     */
    private static final char[] constantChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
    };

    /**
     * Get random String of certain length
     *
     * @param length  the length of generated random string
     * @return        random string of certain length
     */
    public static String randomString(int length) {

        StringBuilder stringBuilder = new StringBuilder(length);

        Random random = new Random();

        for (int i = 0; i < length; i++) {
            int index = random.nextInt(constantChars.length);
            stringBuilder.append(constantChars[index]);
        }

        return stringBuilder.toString();
    }

    /**
     * Checks string is {@code null} or empty("")
     *
     * @param string  the string to be checked
     * @return        {@code true} if string is {@code null} or empty
     */
    public static boolean isEmpty(String string) {
        return org.apache.commons.lang3.StringUtils.isEmpty(string);
    }

    /**
     * Checks string is {@code null}, empty("") or whitespace
     *
     * @param string  the string to be checked
     * @return        {@code true} if string {@code null}, empty or whitespace
     */
    public static boolean isBlank(String string) {
        return org.apache.commons.lang3.StringUtils.isBlank(string);
    }

    /**
     * Replaces place holder ("${}") string in template with values in Map
     *
     * @param template  the template where place holder strings live in
     * @param values    the values Map to replace place holder string
     * @return          string after place holder has been replaced by values
     */
    public static String replacePlaceHolder(String template, Map<String, String> values) {

        if (template.indexOf("${") == -1) {
            return template;
        }

        while (true) {
            int start = template.indexOf("${");
            int end = template.indexOf("}", start);

            if (start != -1 && end != -1) {
                String temp = template.substring(start + 2, end);

                if (values.keySet().contains(temp)) { // contains the dynamic string then replace
                    template = template.substring(0, start) + values.get(temp) + template.substring(end + 1);
                } else {
                    template = template.substring(0, start) + template.substring(end + 1);
                }

            } else {
                break;
            }
        }

        return template;
    }

    /**
     * Replaces place holder with specified interpolation prefix and suffix and values in Map
     *
     * @param template             the template where place holder strings live in
     * @param values               the values Map to replace place holder string
     * @param interpolationPrefix  the interpolation prefix to identify where place holder starts
     * @param interpolationSuffix  the interpolation suffix to identify where place holder ends
     * @return                     new formatted string with all place holders has been replaced by values
     */
    public static String replacePlaceHolder(String template, Map<String, String> values, String interpolationPrefix, String interpolationSuffix) {

        String regExp = Pattern.quote(interpolationPrefix) + "(\\w+)" + Pattern.quote(interpolationSuffix);
        Pattern pattern = Pattern.compile(regExp);
        Matcher matcher = pattern.matcher(template);

        StringBuilder builder = new StringBuilder();
        int index = 0;

        while(matcher.find()) {

            String key = matcher.group(1);

            builder.append(template.substring(index, matcher.start()));
            if (StringUtils.isBlank(key)) {
                builder.append(matcher.group(0));
            } else {
                builder.append(values.get(key));
            }
            index = matcher.end();
        }

        builder.append(template.substring(index, template.length()));
        return builder.toString();
    }

    /**
     * Checks a template string has a place holder symbol("${}")
     *
     * @param template  the template string to be checked
     * @return          {@code true} if it contains "${" string
     */
    public static boolean hasPlaceHolder(String template) {
        if (template.indexOf("${") == -1) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Checks if a string is a template string
     *
     * @param string  the string to be checked
     * @return        {@code true} if it contains "${" string
     */
    public static boolean isTempateString(String string) {
        if (string.indexOf("{") == -1) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Concatenates all elements in the string array
     *
     * @param parts  the string array whose elements to be concatenated
     * @return       a string from concatenating elements in array
     */
    public static String connect(String... parts) {
        StringBuilder sb = new StringBuilder();
        for (String part : parts) {
            sb.append(part);
        }
        return sb.toString();
    }

    /**
     * Concatenates all elements in array in camel style
     *
     * @param parts  the string array to be camelized
     * @return       the camelized string of string array
     */
    public static String camelize(String... parts) {
        StringBuilder sb = new StringBuilder();
        for (String part : parts) {
            sb.append(org.apache.commons.lang3.StringUtils.capitalize(part));
        }
        return org.apache.commons.lang3.StringUtils.uncapitalize(sb.toString());
    }

    /**
     * Converts camel styled template string normal string
     *
     * @param template   the camel styled template string to be converted
     * @param separator  the delimiter string to help judge whether to convert
     * @return           the converted normal string of camel styled template
     */
    public static String convertCamel(String template, String separator) {
        StringBuffer result = new StringBuffer();
        if (template != null && template.length() > 0) {
            result.append(template.substring(0, 1).toLowerCase());
            for (int i = 1; i < template.length(); i++) {
                String s = template.substring(i, i + 1);
                if (s.equals(s.toUpperCase()) && !s.equals(separator)) {
                    result.append(separator);
                    result.append(s.toLowerCase());
                } else {
                    result.append(s);
                }
            }
        }
        return result.toString();
    }

    /**
     * Converts camel styled string to underscore styled string
     *
     * @param camel  the camel styled string
     * @return       the underscore styled string
     */
    public static String convertCamelToUnderscore(String camel) {
        return camel.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
    }

    /**
     * Gets the leftmost length characters of a String
     *
     * @param   string  the String to get the leftmost characters from, may be null
     * @param   length  the length of the required String
     * @return  the leftmost characters, {@code null} if null String input
     */
    public static String left(String string, int length) {
        return org.apache.commons.lang3.StringUtils.left(string, length);
    }

    /**
     * <p>Splits the provided text into an array, separators specified.
     * This is an alternative to using StringTokenizer.</p>
     *
     * <p>The separator is not included in the returned String array.
     * Adjacent separators are treated as one separator.
     * For more control over the split use the StrTokenizer class.</p>
     *
     * <p>A {@code null} input String returns {@code null}.
     * A {@code null} separatorChars splits on whitespace.</p>
     *
     * <pre>
     * StringUtils.split(null, *)         = null
     * StringUtils.split("", *)           = []
     * StringUtils.split("abc def", null) = ["abc", "def"]
     * StringUtils.split("abc def", " ")  = ["abc", "def"]
     * StringUtils.split("abc  def", " ") = ["abc", "def"]
     * StringUtils.split("ab:cd:ef", ":") = ["ab", "cd", "ef"]
     * </pre>
     *
     * @param string     the String to parse, may be null
     * @param separator  the characters used as the delimiters, {@code null}
     *                   splits on whitespace
     * @return           an array of parsed Strings, {@code null} if null
     *                   String input
     */
    public static String[] split(String string, String separator) {
        return org.apache.commons.lang3.StringUtils.split(string, separator);
    }

    /**
     * Finds the value of parameter in a URI query string
     *
     * @param uri    the URI contains query string to search for specific parameter
     * @param param  the name of parameter
     * @return       the value of parameter
     */
    public static String findParameterFromURI(String uri, String param) {
        for(String nameValuePair : org.apache.commons.lang3.StringUtils.split(uri.substring(uri.indexOf("?") + 1), "&")) {
            if(nameValuePair.startsWith(param + "=")) {
                return nameValuePair.substring(nameValuePair.indexOf("=") + 1);
            }
        }
        return null;
    }

    /**
     * Pares the query string's parameters into a Map
     *
     * @param queryString  the query string to be parsed
     * @return             the Map contains all parameters in the query string
     */
    public static Map<String, String> parseQueryString(String queryString) {
        String[] queries = StringUtils.split(queryString, "&");
        Map<String, String> params = new HashMap<String, String>(queries.length);
        for(String query : queries) {
            String[] param = StringUtils.split(query, "=");
            String key = param[0];
            String value = param.length < 2 ? "" : param[1];
              if(params.containsKey(param[0])) {
                params.put(key,  StringUtils.connect(params.get(key), ",", value));
               } else {
                   params.put(key, value);
               }
        }
        return params;
    }

    /**
     * Converts a parameters {@link Map} to a query string form
     *
     * @param params          the {@link Map} contains parameter key and value
     * @param needsURLEncode  a flag to indicate whether to URL encode value
     * @return                the query string of form `key1=value1&key2=value2`
     */
    public static String toQueryString(Map<String, String> params, boolean needsURLEncode) {
        List<String> queries = new ArrayList<String>();
        for(Iterator<Map.Entry<String, String>> iterator = params.entrySet().iterator(); iterator.hasNext();) {
            Map.Entry<String, String> entry = iterator.next();
            String key = entry.getKey();
            String value = entry.getValue();
            if (needsURLEncode) {
                try {
                    value = URLEncoder.encode(value, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    logger.error("StringUtils#toQueryString - URL encodes [{}] encounters UnsupportedEncodingException.", value);
                }
            }
            queries.add(StringUtils.connect(key, "=", value));
        }
        return org.apache.commons.lang3.StringUtils.join(queries, "&");
    }

    /**
     * Converts a parameters {@link Map} to a query string form, without URL
     * encoding the parameter value.
     *
     * @param params  the Map contains parameter key and value
     * @return        the query string of the form `key1=value1&key2=value2`
     * @see           #toQueryString(Map, boolean)
     */
    public static String toQueryString(Map<String, String> params) {
        return toQueryString(params, false);
    }

    /**
     * Converts a parameters {@link Map} to a query string. The query parameter
     * pairs are sorted by the key ascending. Ignoring some parameter pairs is
     * supported by specifying the `ignore` key array.
     *
     * @param params          the {@link Map} contains parameter pairs
     * @param needsURLEncode  a flag to indicate whether to URL encode value
     * @param ignores         the string array of keys to ignore when applying
     * @return                the query string of form `key1=value1&key2=value2`
     *                        and sorted by key ascending
     */
    public static String toSortedQueryString(Map<String, String> params, boolean needsURLEncode, String... ignores) {
        // Sort parameters first
        List<String> keys = new ArrayList<>(params.keySet());
        Collections.sort(keys);

        Map<String, String> parameters = new TreeMap<>();
        for (String key : keys) {
            String value = params.get(key);
            if (!ArrayUtils.contains(ignores, key) && !isBlank(value)) {
                parameters.put(key, value);
            }
        }
        return toQueryString(parameters, needsURLEncode);
    }

    /**
     * Converts a parameters {@link Map} to a query string. The query parameter
     * pairs are sorted by the key ascending. Ignoring some parameter pairs is
     * supported by specifying the `ignore` key array.
     *
     * @param params   the {@link Map} contains parameter pairs
     * @param ignores  the string array of keys to ignore when applying
     * @return         the query string of form `key1=value1&key2=value2`
     * @see            #toSortedQueryString(Map, boolean, String...)
     */
    public static String toSortedQueryString(Map<String, String> params, String... ignores) {
        return toSortedQueryString(params, false, ignores);
    }

    /**
     * Abbreviates a string with ellipsis at the end
     *
     * @param str    the string to be abbreviated
     * @param limit  the length after that the string is abbreviated and appended ellipsis
     * @return       the abbreviated string with ellipsis at the end
     */
    public static String ellipsisString(String str, int limit) {
        if(str != null && str.length() > limit) {
            return str.substring(0, limit) + "...";
        }
        return str;
    }

}
